package org.swift.mashup.engine.model;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Map.Entry;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Templates;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;

import org.swift.commons.xml.NamespaceDefiner;
import org.swift.commons.xml.XmlUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * Utility class for creating XSLT Documents
 * 
 * @author Jonathon Roberts
 * 
 */
public class XsltUtil {

	public static DocumentBuilderFactory factory = DocumentBuilderFactory
			.newInstance();

	public static final String XSLTNS = "http://www.w3.org/1999/XSL/Transform";
	public static final String XSLTVERSION = "2.0";
	public static final String XSLTENCODING = "ISO-8859-1";
	
	public static Document createXslt(NodeList xsltSnipit, String match,
			Collection<String> paramNames, Map<String, String> namespaces, String encoding) throws SAXException {
		
		// Create new stylesheet to be constructed
		final Element styleSheet = newStyleSheet(namespaces);
		try {
			if (isCDDATABlock(xsltSnipit)) {
				xsltSnipit = convertCDataToNodeList(xsltSnipit, namespaces);
			}
		}catch (SAXException sax){
			throw sax;
		} catch (Exception e){
			throw new RuntimeException (e);
		}
		
		// Set rootNode to either the stylesheet or a template based on whether xsl:template exists
		final Element rootNode;
		if (!(isCustomTemplate(xsltSnipit))) rootNode = addTemplate(styleSheet, match); else rootNode=styleSheet;

		// Add all nodes to rootNode checking for xsl:output
		final ArrayList<Element> outputs= new ArrayList<Element>() ;
		XmlUtils.eachNode(xsltSnipit, new XmlUtils.Closure<Node>() {
			public void execute(Node item) {				
				Node newItem = rootNode.appendChild(rootNode.getOwnerDocument()
						.importNode(item, true));		
				if (Element.class.isAssignableFrom(newItem.getClass()) &&  XSLTNS == newItem.getNamespaceURI() && "output".equals(newItem.getLocalName())) outputs.add((Element) newItem);
			}
		});

		// Add output and parameters to stylesheet if required
		final Node first = styleSheet.getFirstChild();
		String docEncoding = encoding!=null ? encoding : XSLTENCODING;
		// Create and output if it does not exist
		if (outputs.size()< 1) outputs.add(styleSheet.getOwnerDocument().createElementNS(XSLTNS, "xsl:output"));
		// Set default output attributes
		final Element output = outputs.get(0);
		if (!output.hasAttribute("method")) output.setAttribute("method", "xml");
		if (!output.hasAttribute("omit-xml-declaration")) output.setAttribute("omit-xml-declaration", "yes");
		if (!output.hasAttribute("encoding")) output.setAttribute("encoding", docEncoding);
		styleSheet.insertBefore(output, first);		
		for (String name : paramNames) {
			addParameter(name, styleSheet, first);
		}

		return styleSheet.getOwnerDocument();

	}

	/**
	 * Ignores any empty text nodes and checks if the return element is a cdatablock.
	 * @param xsltSnipit
	 * @return
	 */
	private static boolean isCDDATABlock(NodeList xsltSnipit) {		
		for (int i = 0; i < xsltSnipit.getLength(); i++) {
			Node node = xsltSnipit.item(i);		
			if (node.getNodeType() == Node.TEXT_NODE
				&& node.getNodeValue().trim().equals("")) {
				continue;
			} else if (node.getNodeType() == Node.CDATA_SECTION_NODE) {
				return true;
			} else {
				return false;
			}
		}
		return false;
	}

	private static NodeList convertCDataToNodeList(NodeList xsltSnipit, Map<String, String> namespaces) throws ParserConfigurationException, SAXException, IOException {

		NamespaceDefiner definer = new NamespaceDefiner(namespaces);
		String snipit = "";
		for (int i = 0; i < xsltSnipit.getLength(); i++) {
			Node node = xsltSnipit.item(i);
			if (node.getNodeType() == Node.CDATA_SECTION_NODE) {
				snipit = node.getNodeValue();
				break;
			}
		}
		
		snipit = definer.defineNamespaces("<root>" + snipit + "</root>");
		
		return XmlUtils.toDocument(snipit).getDocumentElement().getChildNodes();
		
	}

	private static boolean isCustomTemplate(NodeList xsltSnipit) {
		if (xsltSnipit.getLength() < 1) {
			return false;
		}
		return XmlUtils.isElementMatch(xsltSnipit,
				new XmlUtils.Match<Element>() {
					public boolean isMatch(Element node) {
						return XSLTNS.equals(node.getNamespaceURI())
								&& "template".equals(node.getLocalName());
					}
				});
	}

	private static Element addParameter(String name, Element styleSheet,
			Node insertBefore) {
		Element param = (Element) styleSheet.insertBefore(styleSheet
				.getOwnerDocument().createElementNS(XSLTNS, "xsl:param"),
				insertBefore);
		param.setAttribute("name", name);
		return param;
	}

	public static Templates compileXslt(Document doc) throws TransformerConfigurationException {
		TransformerFactory tFactory = TransformerFactory.newInstance();
		return tFactory.newTemplates(new DOMSource(doc));
	}

	public static Templates createTranslet(NodeList xsltSnipit, String match,
			Map<String, String> namespaces, String encoding) throws TransformerConfigurationException, SAXException {
		return createTranslet(xsltSnipit, match, Collections
				.<String> emptySet(), namespaces, encoding);
	}

	public static Templates createTranslet(NodeList xsltSnipit, String match,
			Collection<String> paramNames, Map<String, String> namespaces, String encoding) throws TransformerConfigurationException, SAXException {
		return compileXslt(createXslt(xsltSnipit, match, paramNames, namespaces, encoding));
	}
	
	public static Element newStyleSheet(Map<String, String> namespaces) {

		try {
			DocumentBuilder builder = factory.newDocumentBuilder();

			
			Document doc = builder.parse(new InputSource(new ByteArrayInputStream(
					styleSheetNode(namespaces).getBytes())));

			Element stylesheet = doc.getDocumentElement();

			stylesheet.setAttribute("version", XSLTVERSION);
						
			return (Element) stylesheet;

		} catch (Exception e) {
			throw new RuntimeException("Error creating xslt skeleton", e);
		}
	}

	public static String styleSheetNode(Map<String, String> namespaces) {
		StringBuffer buffer = new StringBuffer("<xsl:stylesheet ");
		if (!namespaces.containsKey("xsl")) {
			buffer.append("xmlns:xsl=\"" + XSLTNS + "\"");
		}
		
		for (Entry<String, String> entry : namespaces.entrySet()) {
			buffer.append(" xmlns:" + entry.getKey() + "=\"" + entry.getValue() + "\"");
		}
		buffer.append(" ></xsl:stylesheet>");
		return buffer.toString();
	}

	public static Element addTemplate(Element stylesheet, String match) {
		if (match == null || "".equals(match)) {
			match = "/";
		}
		Element template = stylesheet.getOwnerDocument().createElementNS(
				XSLTNS, "xsl:template");
		template.setAttribute("match", match);
		return (Element) stylesheet.appendChild(template);
	}

}
